#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<fcntl.h>
#include<errno.h>
#define COMMAND_SIZE 512
#define ARGV_SIZE 32
#define SPACE " "
#define ZERO '\0'
#define NOMODE 0
#define WRITE_TRUNC 1
#define WRITE_APPEND 2
#define READ 3
#define ENDPATH(p) do{  p += strlen(p)-1; while(*p != '/')p--; }while(0)

char cwd[COMMAND_SIZE * 2];
char oldcwd[COMMAND_SIZE * 2];
char* argv[ARGV_SIZE];
int errorcode = 0;
const char* filename;
const char* GetUser()  //获取相应的环境变量内容，下同
{
    const char* ret = getenv("USER");
    if(ret == NULL)
      return "None";
    return ret;
}

const char* GetHostName()
{
    const char* ret = getenv("HOSTNAME");
    if(ret == NULL)
      return "None";
    return ret;
}

const char* GetHome()
{
    const char* ret = getenv("HOME");
    if(ret == NULL)
      return "/";
    return ret;
}

const char* GetPwd()
{
    const char* ret = getenv("PWD");
    if(ret == NULL)
      return "None";
    return ret;
}

const char* GetOldPwd()
{
    const char* ret = getenv("OLDPWD");
    if(ret == NULL)
      return "None";
    return ret;
}
void ShowUserCommand()
{
    char commandline[COMMAND_SIZE];  //定义一个缓冲区，用来存放命令行提示符
    const char* lineuser = GetUser();//获取用户名
    const char* linehostname = GetHostName(); //获取家目录名称
    const char* linepwd = GetPwd();  //获取当前工作路径
    ENDPATH(linepwd);  //得到当前的工作路径后，取出最后一个目录，用宏函数实现
    linepwd++;
    snprintf( commandline, COMMAND_SIZE, "[%s@%s:%s]>> ", lineuser, linehostname, strlen(linepwd) == 0 ? "/" : (strcmp(linepwd, lineuser) == 0 ? "~" : linepwd));
    // 如果长度为0表示当前工作路径为根目录，打印"/",如果当前工作路径是家目录，打印"~"
    printf("%s", commandline); //将缓冲区的内容打印到显示器
    fflush(stdout);
}

int SplitCommand( char* tmp)
{
  //将tmp中的指令切分，读入到命令行参数表argv里
    int ret = NOMODE;
    argv[0] = strtok(tmp,SPACE);
    int i = 1;
    while((argv[i] = strtok(NULL, SPACE))){
      //如果指令中含有 ">>", ">","<"等与重定向相关的字符，则设置返回值ret，对应后续相应的打开文件方式
      if(ret != NOMODE)
      {
        i++;
        continue;
      }
      if(strcmp(argv[i], ">>") == 0)
      {
          ret = WRITE_APPEND;
          filename = strtok(NULL, SPACE); //若有重定向则更新filename，为后面的打开文件做准备,下同
      }
      else if(argv[i][0] == '>')
      {
         if(strlen(argv[i]) == 1)
         {
           ret = WRITE_TRUNC;
           filename = strtok(NULL, SPACE);
         }
         else if(argv[i][1] == '>')
         {
           ret = WRITE_APPEND;
           filename = argv[i] + 2;
         }
         else 
         {
           ret = WRITE_TRUNC;
           filename = argv[i] + 1;
         }
      }
      else if(argv[i][0] == '<')
      {
         ret = READ;
         if(strlen(argv[i]) == 1)
         {
           filename = strtok(NULL, SPACE);
         }
         else 
         {
          filename = argv[i] + 1;
         }
      }
      else 
      {
        i++;
      }
    }
    argv[i] = NULL;
    return ret;
}

int GetUserCommand(char* usercommand, size_t size)
{
  //将一整行输入读取到我们定义的缓冲区 usercommand中，进行后续的解析
    if(NULL == fgets(usercommand, size, stdin))
    {
      return 0;
    }
    usercommand[strlen(usercommand) - 1] = ZERO;
    return strlen(usercommand);//返回缓冲区长度，是0表示没有读到指令
}

void Die()
{
    exit(1);
}
void ChangeFile(int mod)
{
  int fd = 0;
  if(mod == 0) return ;
  if(mod == 1 )
  {
     fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);  //打开重定向的文件，使用相应的模式打开
     dup2(fd,1);                                               //将文件描述符为1（标准输出文件）的文件重定向为fd，即我们打开的文件，下同
  }
  else if(mod == 2)
  {
    fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 0666);
    dup2(fd,1);
  }
  else 
  {
    fd = open(filename, O_RDONLY);
    dup2(fd,0);
  }
}
void RunUserCommand(int mod)
{
    pid_t id = fork();        //执行指令，为子进程运行指令的可执行程序，父进程为shell程序，不能被影响
    if(id == 0)               //子进程
    {
      ChangeFile(mod);        //以mod方式打开文件
      execvp(argv[0], argv);  //进程替换，替换为要执行的指令
      exit(errno);
    }
    else if(id > 0)           //父进程
    {
      int status;
      if(id == waitpid(id, &status, 0) )  //父进程等待子进程，子进程结束后回收，并且记录退出信息status
      {
        errorcode = WEXITSTATUS(status);  //获取退出码
        if(errorcode != 0)                //错误码不为0，则代表子进程执行异常，打印异常信息
          printf("No success:%s:%d\n", strerror(errorcode), errorcode);
      }
    }
    else 
    {
    Die();                    //创建进程失败，退出shell
    }
}

void Cd()
{
    const char* path = argv[1];  //用path记录要进入的目录
    int falg = 0;
    if(path == NULL ||strcmp(path, "~") == 0 ) path = GetHome();  //如果用户没有指定目录，或者目录为"~"就进入家目录
    if(strcmp(path,"-") == 0 )
    {
      falg = 1;
      path = GetOldPwd();    //如果用户要进入的目录为 - ，则为最近一次进入的目录,环境变量oldpwd会记录最近一次进入的目录
    }
    char tmp[COMMAND_SIZE * 2];
    getcwd(tmp,sizeof(tmp)); //将当前工作路径保存，更改路径后用于更新oldpwd

    if( -1 == chdir(path))   //更改工作路径为path
    {
      errorcode =  errno;
      printf("No success:%s:%d\n", strerror(errorcode), errorcode);
      return;
    }
    snprintf(oldcwd, sizeof(oldcwd), "OLDPWD=%s", tmp);  //在缓冲区更新环境变量OLDPWD
    getcwd(tmp,sizeof(tmp));                             //获取更改后的工作路径，用于更新环境变量PWD
    snprintf(cwd, sizeof(cwd),  "PWD=%s", tmp);          //在缓冲区更新环境变量PWD
    putenv(cwd);                                         //更新环境变量PWD
    putenv(oldcwd);                                      //更新环境变量OLDPWD
    if(falg) printf("%s\n", tmp);
}

int CheckBuildin(int mod)
{
  //是内建命令就执行，并且mod不为0时，设置相应的重定向内容
    int ret = 0;
    if(strcmp(argv[0],"cd") == 0 )
    {
      ret = 1;
      Cd();
    }
    else if(strcmp(argv[0],"exit") == 0 )  //用户输入exit就退出shell程序
    {
      ret = 1;
      exit(0);
    }
    else if(strcmp(argv[0], "echo") == 0 && strcmp(argv[1], "$?") == 0 ) //echo $?打印退出码，若有重定向则更换file的指向
    {
      ret = 1;
      FILE* file = stdout;
      if(mod == 1)
        file = fopen(filename, "w");
      else if(mod == 2)
      {
        file = fopen(filename, "a");
      }
      fprintf(file, "%d\n", errorcode);
      errorcode = 0;                      //更新退出码
      if(mod != 0)
        fclose(file);
    }
    return ret;
}

int main()
{
  int quit = 1;
  while(quit)
  {
      //打印命令行提示符
      ShowUserCommand();
      //读取命令行参数
      char usercommand[ARGV_SIZE * 2];
      int n = GetUserCommand(usercommand,sizeof(usercommand));
      if(n < 0) return 1;
      if(n == 0) continue;
      //切分命令行参数
      int mod = SplitCommand(usercommand);
      //判断是否是内建命令
      n = CheckBuildin(mod);
      //执行命令
      if(n == 0)
      RunUserCommand(mod);
  }
  return 0;
}
